import {componentFunction, Recordable} from "./type";
import {isEvent, isGoneProps, isNewProps, isProperty, isSameType} from "./share";

const TEXT_ELEMENT = "TEXT_ELEMENT"

enum FLAG {
    UPDATE = 'UPDATE',
    PLACEMENT = "PLACEMENT",
    DELETION = "DELETION"
}

/**
 * if in tsx we want to define an element we should to write in this <br/>
 * const element = `<h1 title="foo">Hello</h1>`;<br/>
 * tsx will be compiled to js <br/>
 * so compile to js result is : <br/>
 * React.createElement(`h1`, {  title: `foo` }, `Hello`); <br/>
 * so React.createElement is a function, and it's return an object <br/>
 */
// const element = {
//     type: "h1",
//     props: {
//         title: "foo",
//         children: "Hello",
//     },
// }


/**
 * create dom element according to element object  <br/>
 */
// const node = document.createElement(element.type);
// node.title = element.props.title;

/**
 * create text node according to element object  <br/>
 */
// const text = document.createTextNode("");
// text.textContent = element.props.children;

/**
 * append text node to dom element  <br/>
 * append dom element to container  <br/>
 */
// node.appendChild(text);
// container.appendChild(node);

/**
 * create element object
 * @param type
 * @param props
 * @param children
 */
function createElement(type: string | componentFunction, props: Recordable, ...children: any[]) {
    return {
        type,
        props: {
            ...props,
            children: children.map(child => typeof child === 'object' ? child : createTextElement(child))
        }
    }
}

/**
 * create textElement
 * @param text
 */
function createTextElement(text: string) {
    return {
        type: TEXT_ELEMENT,
        props: {
            nodeValue: text,
            children: []
        }
    }
}


function createDom(fiber: Recordable) {
    // create dom according to fiber type
    const dom =
        fiber.type === TEXT_ELEMENT ?
            document.createTextNode("")
            :
            document.createElement(fiber.type);

    // handle props of element except children property
    // Object.keys(fiber.props)
    //     .filter(isProperty)
    //     .forEach((prop: string) => {
    //         dom[prop] = fiber.props[prop]
    //     })
    // return dom
    updateDom(dom, {}, fiber.props);
    return dom
}


function updateDom(dom: HTMLElement, oldProps: Recordable, newProps: Recordable) {
    const isNew = isNewProps(oldProps, newProps)
    const isGone = isGoneProps(oldProps, newProps)

    // remove old or changed event listener
    Object.keys(oldProps)
        .filter(isEvent) // filter is not event
        .filter((key: string) => !(key in newProps) || isNew(key)) //filter is not new prop or  is not new prop value
        .forEach((event: string) => {
            const eventType = event.toLowerCase().slice(2);
            dom.removeEventListener(eventType, oldProps[event])
        })

    // add new or changed event listener
    Object.keys(newProps)
        .filter(isEvent) // filter is not event
        .filter((key: string) => isNew(key)) //filter is not new prop
        .forEach((event: string) => {
            const eventType = event.toLowerCase().slice(2);
            dom.addEventListener(eventType, newProps[event])
        })
    // remove old props
    Object.keys(oldProps)
        .filter(isProperty) // filter is not props
        .filter(isGone) // filter is not gone props
        .forEach((prop: string) => {
            dom[prop] = ''
        })

    // set new or changed props
    Object.keys(newProps)
        .filter(isProperty) // filter is not props
        .filter(isNew) // filter is not new or changed props
        .forEach((prop: string) => {
            dom[prop] = newProps[prop]
        })
}

function commitWork(fiber: any) {
    if (!fiber) {
        return
    }
    let domParentFiber = fiber.parent;
    // find nearest domParent
    while (!domParentFiber.dom) {
        domParentFiber = domParentFiber.parent;
    }
    const domParent = domParentFiber.dom;

    if (fiber.effectTag === FLAG.PLACEMENT && fiber.dom !== null) {
        // create a new dom
        domParent.appendChild(fiber.dom);
    } else if (fiber.effectTag === FLAG.UPDATE && fiber.dom !== null) {
        // update dom props
        updateDom(fiber.dom, fiber.alternate.props, fiber.props)
    } else if (fiber.effectTag === FLAG.DELETION) {
        // delete old dom
        commitDeletion(fiber, domParent)
    }
    commitWork(fiber.child);
    commitWork(fiber.sibling);
}

/**
 * delete old dom
 * @param fiber
 * @param domParent
 */
function commitDeletion(fiber: any, domParent: HTMLElement) {
    if (fiber.dom) {
        domParent.removeChild(fiber.dom)
    } else {
        commitDeletion(fiber.child, domParent)
    }
}

/**
 * wipRoot is build in render function
 * so wipRoot.child is React Root dom
 * ultimately reset wipRoot
 */

function commitRoot() {
    // process deleted nodes
    deletions.forEach(commitWork)
    commitWork(wipRoot.child);
    currentRoot = wipRoot;
    wipRoot = null;
}


let nextUnitOfWork = null;
// work in progress fiber root
let wipRoot = null;

let currentRoot = null;

let deletions = null;

let wipFiber = null;

let hookIndex = null;

function workLoop(deadLine: IdleDeadline) {
    let shouldYield = false;
    while (nextUnitOfWork && !shouldYield) {
        nextUnitOfWork = perFormUnitOfWork(
            nextUnitOfWork
        )
        // https://developer.mozilla.org/zh-CN/docs/Web/API/IdleDeadline/timeRemaining
        shouldYield = deadLine.timeRemaining() < 1
    }
    // if no nextUnitOfWork and wipRoot is existed
    // commit root
    if (!nextUnitOfWork && wipRoot) {
        commitRoot()
    }
    // https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestIdleCallback
    requestIdleCallback(workLoop)
}

requestIdleCallback(workLoop);

function reconcileChildren(wipFiber: Recordable, elements: any[]) {
    let index = 0;
    let oldFiber = wipFiber.alternate && wipFiber.alternate.child;
    let prevSibling = null;
    while (
        index < elements.length ||
        oldFiber
        ) {
        const element = elements[index];
        const sameType = isSameType(oldFiber, element);
        let newFiber = null;
        // compare oldFiber to element
        if (sameType) {
            // update the  node
            newFiber = {
                type: oldFiber.type,
                // props need use new props
                props: element.props,
                dom: oldFiber.dom,
                parent: wipFiber,
                alternate: oldFiber,
                effectTag: FLAG.UPDATE
            }
        }
        if (element && !sameType) {
            // add this node
            newFiber = {
                type: element.type,
                props: element.props,
                dom: null,
                parent: wipFiber,
                alternate: null,
                effectTag: FLAG.PLACEMENT
            }
        }
        if (oldFiber && !sameType) {
            // delete this oldFiber's node
            oldFiber.effectTag = FLAG.DELETION;
            deletions.push(oldFiber);
        }
        if (oldFiber) {
            oldFiber = oldFiber.sibling
        }

        if (index === 0) {
            wipFiber.child = newFiber
        } else if (element) {
            prevSibling.sibling = newFiber
        }
        prevSibling = newFiber
        index++
    }
}

/**
 * start workLoop
 * @param fiber
 */
function perFormUnitOfWork(fiber: Recordable) {

    // check if fiber is function component
    const isFunctionComponent = fiber.type instanceof Function;
    if (isFunctionComponent) {
        updateFunctionComponent(fiber)
    } else {
        updateHostComponent(fiber)
    }
    // // add dom node
    // if (!fiber.dom) {
    //     fiber.dom = createDom(fiber)
    // }

    // if (fiber.parent) {
    //     fiber.parent.dom.appendChild(fiber.dom)
    // }

    // const elements = fiber.props.children;
    // reconcileChildren(fiber, elements);
    if (fiber.child) {
        return fiber.child
    }
    let nextFiber = fiber;
    while (nextFiber) {
        if (nextFiber.sibling) {
            return nextFiber.sibling
        }
        nextFiber = nextFiber.parent;
    }
}

/**
 * update function component
 * @param fiber
 */
function updateFunctionComponent(fiber: Recordable) {
    wipFiber = fiber;
    hookIndex = 0;
    wipFiber.hooks = [];
    const children = [fiber.type(fiber.props)];
    reconcileChildren(fiber, children)
}

/**
 * update host component
 * @param fiber
 */
function updateHostComponent(fiber: Recordable) {
    if (!fiber.dom) {
        fiber.dom = createDom(fiber)
    }
    reconcileChildren(fiber, fiber.props.children)
}

/**
 * render element to container
 * @param element
 * @param container
 */
function render(element: Recordable, container: HTMLElement) {
    // set nextUnitOfWork
    wipRoot = {
        dom: container,
        props: {
            children: [element]
        },
        alternate: currentRoot
    }
    nextUnitOfWork = wipRoot;
    deletions = []
}

function useState(initial: any) {
    const oldHook = wipFiber.alternate && wipFiber.alternate.hooks && wipFiber.alternate.hooks[hookIndex]
    const hook = {
        state: oldHook ? oldHook.state : initial,
        queue: [],
    }
    const actions = oldHook ? oldHook.queue : [];
    actions.forEach((action: (arg: any) => any) => {
        hook.state = action(hook.state)
    })
    const setState = (action: any) => {
        hook.queue.push(action);
        wipRoot = {
            dom: currentRoot.dom,
            props: currentRoot.props,
            alternate: currentRoot
        }
        nextUnitOfWork = wipRoot;
        deletions = [];
    }
    wipFiber.hooks.push(hook);
    hookIndex++;
    return [hook.state, setState]
}

/**
 * this is your react
 */
const EReact = {
    createElement,
    render,
    useState,
}

/**
 * export your react
 */
export default EReact;



